package enc;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import org.objenesis.ObjenesisStd;

import titt.io.IOFactory;
import titt.io.encoding.EncodingFacade;
import titt.type.Type;
import titt.type.Types;
import titt.util.Bytes;

public class ObjectEncoder {

	private EncodingFacade ef = IOFactory.createEncodingFacade();

	public byte[] encode(Type t, Object o) throws Exception {
		return encode(t,o,new HashMap<Object, Integer>());
	}

	public byte[] encode(Type t, Object o, Map<Object, Integer> refs) throws Exception {
		if(t.isNative()) {
			if(refs.containsKey(o))
				return Bytes.asBytes(ef.encodeToString(Types.toArray(Types.INT), new int[]{refs.get(o)}) + "$");
			
			StringBuffer sb = new StringBuffer();

			Class<?> cl = o.getClass();

			int[] markup = new int[1 + 3 * cl.getDeclaredFields().length];
			int c = 0;

			int ref = refs.size();

			markup[c++] = ref;

			refs.put(o, ref);

			for(Field f : cl.getDeclaredFields()) {
				if(!f.isAccessible())
					f.setAccessible(true);

				Type ty = new Type(f.getType());

				String name = ef.encodeToString(Types.STRING, f.getName());
				String type = ty.asString();
				String fieldEnc = Bytes.asString(encode(ty, f.get(o), refs));

				markup[c++] = name.length();
				markup[c++] = type.length();
				markup[c++] = fieldEnc.length();

				sb.append(name);
				sb.append(type);
				sb.append(fieldEnc);
			}

			sb.insert(0, "$");
			sb.insert(0, ef.encodeToString(Types.toArray(Types.INT), markup));

			return Bytes.asBytes(sb.toString());
		}
		return ef.encode(t, o);
	}

	public Object decode(Type t, byte[] b) throws Exception {
		return decode(t,b,new HashMap<Integer, Object>());
	}
	
	public Object decode(Type t, byte[] b, Map<Integer, Object> decoded) throws Exception {

		if(t.isNative()) {
			String s = Bytes.asString(b);


			int markupIndex = s.indexOf("$");

			String markupEnc = s.substring(0, markupIndex);

			s = s.substring(markupIndex+1);

			int[] markup = (int[])ef.decodeFromString(Types.toArray(Types.INT), markupEnc);
			
			if(decoded.containsKey(markup[0]))
				return decoded.get(markup[0]);

			Object o = new ObjenesisStd().newInstance(t.asClass());
			
			decoded.put(markup[0], o);

			for(int i = 1; i < markup.length; i+=3) {
				String fname = (String)ef.decodeFromString(Types.STRING, s.substring(0, markup[i]));
				s = s.substring(markup[i], s.length());
				
				Type ft = new Type(s.substring(0, markup[i+1]));
				s = s.substring(markup[i+1], s.length());

				byte[] fb = Bytes.asBytes(s.substring(0, markup[i+2]));
				s = s.substring(markup[i+2], s.length());

				for(Field f : o.getClass().getDeclaredFields()) {
					if(!f.isAccessible())
						f.setAccessible(true);
					if(f.getName().equals(fname))
						f.set(o, decode(ft, fb, decoded));
				}
			}

			return o;
		}
		return ef.decode(t, b);
	}

	public static void main(String[] args) throws Exception {
		ObjectEncoder e = new ObjectEncoder();

		Type t = new Type(TestObject.class);

		TestObject oto = new TestObject("foobar", new int[]{0,1,2,3,4,5,6});

		System.out.println(oto.getState());
		System.out.println(oto.getSO().getData()[0]);
		System.out.println(oto == oto.getSO().getParent());

		byte[] enc = e.encode(t, oto);
		
		System.out.println();
		System.out.println(Bytes.asString(enc));
		System.out.println();
		
		Object d = e.decode(t, enc);
		TestObject to = (TestObject)d;
		
		
		System.out.println(to.getState());
		System.out.println(to.getSO().getData()[0]);
		System.out.println(to == to.getSO().getParent());
	}

}
